深入学习Java RMI反序列化

RMI 概述

Java RMI全称为 java Remote Method Invocation(java 远程方法调用),是java编程语言中,一种实现远程过程调用的应用程序编程接口。存储于java.rmi包中,使用其方法调用对象时,必须实现Remote远程接口,能够让某个java虚拟机上的对象调用另外一个Java虚拟机中的对象上的方法。两个虚拟机可以运行在相同计算机上的不同进程,也可以是网络上的不同计算机。

RMI 系统使用现有的 Web 服务器从服务端到客户端以及从客户端到服务器进行通信

RMI组成

RMI一般由三部分组成,分别是Server(服务段)、Client(客户端)和Registry(注册中心)

客户端

客户端是调用远程对象的发起者,它会将需要调用的方法以及参数传递到Client Stub,在Client Stub进行序列化(Marshalling),然后以二进制的方式传输到服务端

注册中心(Registry)

Registry用于远程服务的管理,它可以提供服务的查询,绑定,解绑,重绑等操作。Server端的服务需要先绑定在Registry才能被Client调用

服务端(Server)

服务端是RMI中的被调用者,它接收到Client发送到二进制数据后,会在传递给Skeleton,然后调用RemoteCall将客户端传来的序列化数据进行反序列化(Unmarshalling),然后进行本地调用,将结果传递给客户端

大致流程

RMI底层通讯采用了Stub(运行在客户端)Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
  2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象
  3. RemoteCall序列化RMI服务名称Remote对象
  4. RMI客户端远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端远程引用层
  5. RMI服务端远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化
  7. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端
  8. RMI客户端反序列化服务端结果,获取远程对象的引用
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端
  10. RMI客户端反序列化RMI远程方法调用结果

Demo1

定义了一个Client和一个Server,他们都定义了一个接口,这个接口需要继承Remote接口

RMIServer

IRemoteObj接口

public interface IRemoteObj extends Remote {
    public String sayHello(String keywords) throws RemoteException;
}

这是一个接口,继承了Remote接口

RemoteObjImpl

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {

    public RemoteObjImpl() throws RemoteException {
       //UnicastRemoteObject.exportObject(this,1099);//如果不继承UnicastRemoteObject就需要手工导出
    }

    @Override
    public String sayHello(String keywords) throws RemoteException {
        String upKeywords = keywords.toUpperCase();//转大写
        System.out.println(upKeywords);
        return upKeywords;
    }
}

这是一个Server端绑定的对象类,实现了接口的方法。

注意:

  • 只有继承了接口并实现了其中的方法,才能被远程调用。
  • 这个类需要继承UnicastRemoteObject,只有继承了UnicastRemoteObject的类,才能作为远程对象而被客户端调用。

RMIServer

public class RMIServer {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
        IRemoteObj remoteObj = new RemoteObjImpl();//新建Impl对象
        Registry registry = LocateRegistry.createRegistry(1099);//注册1099端口
        registry.bind("remoteObj",remoteObj);
        //在1099端口中,对这个名称和类进行绑定,客户端只需要查找这个名称即可查找到对应的类。
        System.out.println("Server is running ....");
    }
}

RMIClient

客户端就只需要定义一个接口和一个Client就行了

IRemoteObj接口

public interface IRemoteObj extends Remote {
    public String sayHello(String keywords) throws RemoteException;
}

RMIClient

public class RMIClient {
    public static void main(String[] args) throws RemoteException, NotBoundException {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);//客户端从URL中获取远程对象
        IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");//lookup函数从远程端口中寻找这个类名
        remoteObj.sayHello("hello");//调用这个远程类的方法
    }
}

运行效果:

image-20220506154417127
image-20220506154429570
image-20220506154440719

发现在Server端,将传入的hello转为了大写并打印,因为在Client并没有将remoteObj.sayHello("hello")的返回值进行一个打印输出

String Hello = remoteObj.sayHello("hello");
System.out.println(Hello);
image-20220506154641763

这样便可以在客户端输出结果了。

注意: 服务器和客户端的接口、接口包名、接口名称、接口内容都必须一样。甚至两者之间传输的实体的包名也必须一致。

否则就会抛出ClassNotFoundException异常

image-20220506155039155

流程分析

img

这个流程图很好理解 ,首先注册中心会对名字远程对象做一个绑定,然后服务端发布远程对象,最后客户端连接注册中心从而调用远程对象,传输使用的Socket,使用了JRMP协议。

但实际上并不是客户端能够直接调用服务端,而是使用了代理,客户端的代理叫Stub,服务端的代理叫Skeleton。把业务之外的东西提取出来,让代理对象去做,例如一些网络请求等

创建远程服务

来调试分析一下创建服务端远程对象的流程

image-20220513092855895

首先来到RemoteServer,这里是一个静态赋值,是因为远程对象类RemoteObjImpl有父类UnicastRemoteObject,它这里面的静态值的赋值都是在构造函数之前完成的。

image-20220513093057368

然后就来到了远程对象的构造函数,现在已经有这个对象了,但是要把它发布到网上去,现在来看一下,发布到网上去的这个过程

然后继续跟进,会先进入到父类的构造函数

protected UnicastRemoteObject() throws RemoteException
{
    this(0);
}
protected UnicastRemoteObject(int port) throws RemoteException
{
    this.port = port;
    exportObject((Remote) this, port);
}

这个port是默认值0,然后会把远程对象发布到一个随机的端口上

image-20220513094701348

所以说如果不继承UnicastRemoteObject就需要手动调用这个UnicastRemoteObject#exportObject()函数,如果继承了这个父类,那么这些都可以在构造函数中完成。第一个参数传入的我们的远程对象,第二个参数就是实例化一个处理网络逻辑的对象。

跟进这个UnicastServerRef,然后又实例化了一个LiveRef

image-20220513095105752

继续跟进到LiveRef

image-20220513095511595

这里就是实例化了一个ID,就不管了,来看LiveRef的构造函数

image-20220513095817154

这里第一个参数是ID,第二个参数是TCPEndpoint.getLocalEndpoint(port),然后第三个参数是true,这里来看一下getLocalEndpoint,这个方法的返回值是一个网络请求的类

image-20220513095905016

来看看他的构造函数,两个参数,一个ip,一个端口。

image-20220513101649513

再回到刚才这里,继续跟进

image-20220513101759961
image-20220513101853180

这里一共有三个东西,第一个就是刚刚传入的endpoint,第二个就是ID,第三个是isLocal

image-20220513102134946

TCPEndpoint里面一个自己的IP,一个端口(目前默认还是0),然后注意这里有一个TCPTransport,这是真正处理网络请求的东西。这段过程就相当于一直在封装,直到TCPTransport才真正到了处理网络请求的地方。

然后回到刚才的地方

image-20220513102458670

这里调用了父类的构造函数,跟进去看一下

public UnicastRef(LiveRef liveRef) {
    ref = liveRef;
}

他叫UnicastServerRef,父类叫UnicastRef,这里也就简单赋了一个值,让ref 等于 传入进来的liveRef对象

image-20220513102806387

从始至终就只创建了这一个LiveRef对应着远程服务的端口。

image-20220513102923232

然后这里UnicastServerRef的实例化过程就走完了,我们继续跟进到exportObject的流程

private static Remote exportObject(Remote obj, UnicastServerRef sref)
    throws RemoteException
{
    // if obj extends UnicastRemoteObject, set its ref.
    if (obj instanceof UnicastRemoteObject) {
        ((UnicastRemoteObject) obj).ref = sref;
    }
    return sref.exportObject(obj, null, false);
}
image-20220513103809862

这里走完还是在调用 UnicastServerRef#exportObject(),跟进去发现是创建代理的过程

image-20220513104610105

有一个疑问: 这是服务端的远程服务创建过程,可是stub是客户端的代理对象啊?

原因就是: 它的流程是先在服务端创建好,然后放到注册中心,然后客户端从注册中心拿到stub,拿到之后再操作stub代理,然后通过它来操作Remote Skeleton,然后再真正的调用服务端的远程对象。

image-20220513104910082

现在跟进到createProxy()方法

image-20220513111001806

implClass就是我们的远程对象类,clienRef里面就是刚才创建的LiveRef(核心)

然后后续就是创建动态代理的过程

image-20220513112411301

然后后面这里有一个Target,这个相当于一个总封装,把目前创建的有用的都放到这里面,然后调用LiveRef#exportObject()方法,把target发布出去

image-20220513113825625

然后LiveRef#exportObject()方法又调用了TCPEndpoint#exportObject()方法

image-20220513114916638

一直跟进,最后是走到了TCPTransport#exportObject()方法了,

image-20220513115042814

这里第一步是listen(),也就意味着会开端口之类的,跟进去看看

image-20220513115901180

这里先获取到刚才刚才的TCPEndpoint

image-20220513115958675

这里创建了一个新的socket,创建了一个新的线程,然后开启,这个线程就是用来处理连接之后的逻辑的。

image-20220513120401549
image-20220513120502704

这里面就是网络请求的具体操作了,然后等待连接就行了。

注意TCPTransportlisten()方法中调用的newServerSocket()。因为一开始的端口默认为0嘛,然后这里做了一个判断,如果为0的话,就会随机设置一个端口

image-20220513120905893
image-20220513120808781

到目前位置,远程对象已经被发布出去了,但是是在一个随机的端口上,客户端是不知道的,而客户端在向远程服务端请求的时候又需要指明端口。这个时候远程对象已经发布出去了,后续跟进到这里会看到对发布的信息进行一个记录

image-20220513121812622
image-20220513122016279

这里objTable.put(oe, target)implTable.put(weakImpl, target) 是两个存储的表,它里面放的就是远程对象整个的完整封装体target,服务端把这些信息全都保存在了这两个表里面。

到这里差不多整个服务端的远程对象发布过程就结束了,大概的一个调试流程图

image-20220513122541073

创建注册中心&绑定

打上断点,进行调试分析

image-20220515102615572
image-20220515102720669

来到了LocateRegistry#createRegistry()方法,传入了一个1099的端口,然后创建了一个RegistryImpl这个对象

image-20220515103028980

这里创建了一个LiveRef和一个UnicastServerRef,然后端口是1099

image-20220515103659335

跟进到UnicastServerRef

public UnicastServerRef(LiveRef ref) {
    super(ref);
}
public UnicastRef(LiveRef liveRef) {
    ref = liveRef;
}

这里还是一个简单的赋值操作,接下来看看setup

private void setup(UnicastServerRef uref)
    throws RemoteException
{
    /* Server ref must be created and assigned before remote
     * object 'this' can be exported.
     */
    ref = uref;
    uref.exportObject(this, null, true);
}

这里调用了UnicastServerRef#exportObject(),而之前创建远程服务的时候调用的是UnicastRemoteObject#exportObject()

image-20220515111404634

这两者的区别就是,创建远程服务的时候,第三个参数是false,而这里创建注册中心的时候第三个参数是true。

image-20220515112703690

第三个参数的意思就是: 是否永久性,所以远程服务是临时的,注册中心是永久的。

image-20220515113115103

这里创建了stub,用来给后续客户端使用。

image-20220515113319923
image-20220515115536425

这个文件名+"_Stub" 如果在系统中存在的话,就会返回true,没有的话就会返回false。这里如果需要走到createStub的话,stubClassExists(remoteClass)需要为真。在jdk源码是自带这个类的。

image-20220515120414246
image-20220515115844600

这里就通过反射获取stub的构造器,然后实例化一个Stub出来。然后当前的Stub就是RegistryImpl_Stub,之前远程服务的Stub是动态代理创建出来的

image-20220515120818086
image-20220515121323600

然后又继续判断这个stub对象是不是RemoteStub类的实例,如果是的话,就调用setSkeleton()方法

public void setSkeleton(Remote impl) throws RemoteException {
    if (!withoutSkeletons.containsKey(impl.getClass())) {
        try {
            skel = Util.createSkeleton(impl);
        } catch (SkeletonNotFoundException e) {
            /*
             * Ignore exception for skeleton class not found, because a
             * skeleton class is not necessary with the 1.2 stub protocol.
             * Remember that this impl's class does not have a skeleton
             * class so we don't waste time searching for it again.
             */
            withoutSkeletons.put(impl.getClass(), null);
        }
    }
}

然后会调用Util.createSkeleton(impl)

1652589397756.png

这里跟刚刚创建Stub的过程一样,都是通过反射来实例化。

1652589574110.png
Target target =
    new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);

然后也是通过Target来存储所有的信息,然后exportObject来发布,走到后面的ObjectTable来看一下都存储了什么信息。

打开之后发现了里面有三个远程对象,但我们只创建了一个RemoteImpl和RegistryImpl,这里却出现了一个DGCImpl。这其实是分布式垃圾回收,是默认创建的。

1652589949350.png

然后这是使用动态代理创建远程服务时候的远程对象,此时引用里的skel是空的

image-20220515124650346

然后这是创建注册中心的时候的远程对象,此时引用里的skel就是刚刚的RegistryImpl_Skel

image-20220515125251294

到这里注册中心就已经创建好了,接下来看看绑定的流程。

1652590751392.png

可以看到此时的远程对象和注册中心都已经创建好了。

然后跟进去看一下绑定流程,这里是一个HashTable,然后里面装的是已经绑定的远程对象名,我们这里取名为remoteObj,如果已经存在这个名字了,就会抛出一个异常,如果不存在的话,就会将这个name远程对象给放入这个HashTable中,然后完成了绑定。

image-20220515130123285

客户端请求注册中心——客户端

客户端请求注册中心一般会做两件事:

  1. 向注册中心获取到这个远程对象的代理(Stub)
  2. 通过这个代理向服务端去做真正的远程调用
image-20220515160900777

首先客户端会通过传入的IP和端口去获取一个注册中心的Stub,来看一下这部分是如何获取的?

image-20220515161234671

这里传入了IP和端口之后,就创建了一个LiveRef,然后把IP端口传入进去,然后封装了一下,然后就调用了createProxy(),在服务端创建远程服务的时候,也调用过这个。

public static Remote createProxy(Class<?> implClass,
                                 RemoteRef clientRef,
                                 boolean forceStubUse)
    throws StubNotFoundException
{
    Class<?> remoteClass;

    try {
        remoteClass = getRemoteClass(implClass);
    } catch (ClassNotFoundException ex ) {
        throw new StubNotFoundException(
            "object does not implement a remote interface: " +
            implClass.getName());
    }

    if (forceStubUse ||
        !(ignoreStubClasses || !stubClassExists(remoteClass)))
    {
        return createStub(remoteClass, clientRef);
    }

实际上客户端的Stub并不是直接从注册中心传过来的,而是只传了参数,然后在本地重新创建了一个Stub,在客户端的Stub和注册中心的Stub实际上是完全一样的。

private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
    throws StubNotFoundException
{
    String stubname = remoteClass.getName() + "_Stub";

    /* Make sure to use the local stub loader for the stub classes.
     * When loaded by the local loader the load path can be
     * propagated to remote clients, by the MarshalOutputStream/InStream
     * pickle methods
     */
    try {
        Class<?> stubcl =
            Class.forName(stubname, false, remoteClass.getClassLoader());
        Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
        return (RemoteStub) cons.newInstance(new Object[] { ref });
        } catch (ClassNotFoundException e) {
            throw new StubNotFoundException(
                "Stub class not found: " + stubname, e);
image-20220515162821159

然后就获取到了注册中心的Stub,下一步就是获取远程对象,把name传进去,然后获取到远程对象的代理。

这里继续跟进,走到了sun.rmi.server.UnicastRef#newCall()方法,按道理来说应该走到sun.rmi.registry.RegistryImpl_Stub#lookup()方法,这里出现bug的原因是因为RegistryImpl_Stub是反编译出来的,版本是1.1,而我们当前使用的jdk1.8,所以就出现了一下bug,行号对不上,也就调试不了了,那就静态的来分析一下代码。

image-20220515163555935

这里传进来一个字符串,也就是name,然后把它序列化了,然后下一步调用了UnicastRef#invoke()

image-20220515163732017
image-20220515163904611

然后调用了executeCall()方法, 这就是一个处理网络请求的方法,客户端的网络请求都是通过这个方法来实现了

image-20220515164005762

这里从super.ref.invoke(var2);UnicastRef的call.executeCall();StreamRemoteCall.executeCall()

image-20220515165034719

这个函数里面有一个处理异常的地方,如果是这个2号异常的话,就会通过反序列化来获取这个流离的对象,设计的本意可能是一个异常类,通过反序列化来获得更详细的信息,如果注册中心返回一个恶意的对象流,客户端就会在这里进行反序列化,这个地方的反序列化会很隐蔽。因为所有的处理网络请求的都会调用这个executeCall()方法

image-20220515165326942

这里建立了一个请求,完成请求之后,又获取了一个输入流,然后通过反序列化的把这个网络请求的返回值读取出来了,这里的var23就是远程获取到的动态代理的对象。

image-20220515164133124

客户端向注册中心获取远程对象代理的这个过程是通过反序列化实现的,如果有一个恶意的注册中心,也可以通过这个方式来攻击这个客户端

image-20220515203352279

最后就获取到了这个远程对象,然后下一步就是客户端直接去连接服务

客户端请求服务端——客户端

image-20220515205606029

这里是走到了RemoteObjectInvocationHandler#invoke(),因为获取到的是一个动态代理,所以调用任何方法,都会走到调用处理器的invoke()方法

最后调用了invokeRemoteMethod()

image-20220515211625348
image-20220515211838623

然后调用了一个重载的UnicastRef#invoke()方法,跟进去,这里也是创建了一个连接

image-20220515212140653
1652622440547.png

这里有一个marshalValue(),它的作用是把传入的参数序列化,比如这里传入的hello,序列化之后传给服务端。

1652622851011.png

判断是否是基础类型,如果不是基础类型的话,就把它序列化。

然后又调用了executeCall(),所有的网络请求都会调用这个方法。

1652623266949.png

后面如果调用这个函数是有返回值的话,会调用unmarshalValue()来反序列化获取这个返回值

1652624260391.png
image-20220515221852524
1652624471462.png

因为这里是String类型的,所以这些if都不满足,可以走到反序列化这里读取返回值。

最后走完,成功获取到了返回值并且打印输出。

image-20220515222320328

在客户端请求服务端,真正调用这个远程对象的时候,也是存在两个反序列化的点:

​ 1、第一个点是网络请求的时候executeCall() 的时候。

image-20220515222743744

这部分处理网络请求的协议就叫做JRMP协议

​ 2、第二个点是从远程调用方法后,通过反序列化获取返回值的时候。

客户端请求注册中心-注册中心

客户端请求服务端-服务端

客户端请求服务端-dgc

JDK高版本绕过

总结

参考

@白日梦组长

@ttpfx

@javasec

@先知社区